import { opponentColor, Piece, PieceColor, PieceType } from './Piece';
import { Move } from './Move';
import { range } from '../utils/range';
import { Board } from './Board';
import { Rules } from './Rules';

export enum GameState {
    NORMAL = 0,
    CHECK = 1,
    CHECKMATE = 2,
    STALEMATE = 3,
    SETTLED = 4,
}

export interface Threat {
    position: { row: number, col: number };
}

export function isThreatEqual(a: Threat, b: Threat): boolean {
    return a.position.row === b.position.row && a.position.col === b.position.col;
}

export interface ThreatReport {
    state: GameState;
    threats: Threat[];
}

export class Judge {
    constructor(board: Board, rules: Rules) {
        this.board = board;
        this.rules = rules;
    }

    public analyze(player: PieceColor): ThreatReport {
        const board = this.board;
        const allPlayerPieces = board.getPiecesOfColor(player);
        // is game settled?
        if (allPlayerPieces.findIndex(it => it.type === PieceType.GENERAL) < 0) {
            // yes
            return {
                state: GameState.SETTLED,
                threats: []
            };
        } else {
            // is player in check?
            const threatReport = this.analyzeThreat(player);
            if (threatReport.state === GameState.CHECK) {
                // is player in checkmate?
                const couldResolveCheck = allPlayerPieces
                    .map(it => this.enumerateAllValidMoves(it)).reduce((a, v) => a.concat(v), [])
                    .map(it => this.evaluateMove(player, it))
                    .reduce((a, v) => a || v, false);
                if (couldResolveCheck) {
                    return threatReport;
                } else {
                    return {
                        state: GameState.CHECKMATE,
                        threats: threatReport.threats,
                    };
                }
            } else {
                // is player in stalemate?
                const validMoves = allPlayerPieces
                    .map(it => this.enumerateAllValidMoves(it))
                    .reduce((a, v) => a.concat(v), []);
                const moveAvailable = validMoves
                    .map(it => this.evaluateMove(player, it))
                    .reduce((a, v) => a || v, false);
                if (moveAvailable) {
                    return threatReport;
                } else {
                    return {
                        state: GameState.STALEMATE,
                        threats: threatReport.threats
                    };
                }
            }
        }
    }

    public evaluateMove(player: PieceColor, move: Move): boolean {
        // XXX: pretty expensive to save and load
        // The stress test has shown that most of operations
        // could be finished in under 100ms.
        const boardSnapshot = this.board.save();
        if (this.board.tryMove(move)) {
            const newThreatReport = this.analyzeThreat(player);
            this.board.load(boardSnapshot);
            return newThreatReport.state !== GameState.CHECK;
        } else {
            return false;
        }
    }

    private analyzeThreat(player: PieceColor): ThreatReport {
        const allOpponentPieces = this.board.getPiecesOfColor(opponentColor(player));
        const possibleAttacks = allOpponentPieces
            .map(it => this.enumerateAllAttackingMoves(it))
            .reduce((a, v) => a.concat(v), []);
        const possibleThreats = possibleAttacks.map(it => {
            const { row, col } = it.getSourcePiece(this.board);
            return {
                position: {
                    row: row,
                    col: col
                }
            };
        });
        const checkIndex = possibleAttacks.findIndex(it => it.getTargetPiece(this.board).type === PieceType.GENERAL);
        if (checkIndex >= 0) {
            return {
                state: GameState.CHECK,
                threats: possibleThreats
            };
        } else {
            return {
                state: GameState.NORMAL,
                threats: possibleThreats
            };
        }
    }

    public enumerateAllValidMoves(piece: Piece): Move[] {
        switch (piece.type) {
            case PieceType.PAWN:
                return [
                    Move.fromOffset(piece, 1, 0),
                    Move.fromOffset(piece, -1, 0),
                    Move.fromOffset(piece, 0, 1),
                    Move.fromOffset(piece, 0, -1)
                ].filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.KNIGHT:
                return [
                    Move.fromOffset(piece, 1, 2),
                    Move.fromOffset(piece, 2, 1),
                    Move.fromOffset(piece, -1, 2),
                    Move.fromOffset(piece, 2, -1),
                    Move.fromOffset(piece, 1, -2),
                    Move.fromOffset(piece, -2, 1),
                    Move.fromOffset(piece, -1, -2),
                    Move.fromOffset(piece, -2, -1)
                ].filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.BISHOP:
                return [
                    Move.fromOffset(piece, 2, 2),
                    Move.fromOffset(piece, -2, 2),
                    Move.fromOffset(piece, -2, -2),
                    Move.fromOffset(piece, 2, -2)
                ].filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.GENERAL:
                return [
                    Move.fromOffset(piece, 0, 1),
                    Move.fromOffset(piece, 1, 0),
                    Move.fromOffset(piece, 0, -1),
                    Move.fromOffset(piece, -1, 0)
                ].concat(
                    range(5, 9)
                        .map(it => [
                            new Move(piece.row, piece.col, piece.row + it, piece.col),
                            new Move(piece.row, piece.col, piece.row - it, piece.col)
                        ])
                        .reduce((a, v) => a.concat(v), [])
                ).filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.ADVISOR:
                return [
                    Move.fromOffset(piece, 1, 1),
                    Move.fromOffset(piece, 1, -1),
                    Move.fromOffset(piece, -1, -1),
                    Move.fromOffset(piece, -1, 1)
                ].filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.ROOK:
            case PieceType.CANNON:
                return range(0, 9)
                    .map(it => new Move(piece.row, piece.col, it, piece.col))
                    .concat(
                        range(0, 8).map(it => new Move(piece.row, piece.col, piece.row, it))
                    )
                    .filter(it => this.rules.validateMove(this.board, it, piece.color));
            case PieceType.EMPTY:
            default:
                return [];
        }
    }

    public enumerateAllAttackingMoves(piece: Piece): Move[] {
        return this.enumerateAllValidMoves(piece)
            .filter(it => it.getTargetPiece(this.board).type !== PieceType.EMPTY);
    }

    private board: Board;
    private rules: Rules;
}
